package cn.seaboot.admin.mvc.cors;

import org.jetbrains.annotations.NotNull;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.Nullable;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.util.UrlPathHelper;

import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Provide a per request {@link CorsConfiguration} instance based on a
 * collection of {@link CorsConfiguration} mapped on path patterns.
 *
 * <p>Exact path mapping URIs (such as {@code "/admin"}) are supported
 * as well as Ant-style path patterns (such as {@code "/admin/**"}).
 *
 * @author Mr.css
 * @version 2023-01-28 16:06
 * @see org.springframework.web.cors.UrlBasedCorsConfigurationSource 代码根据这个类进行改造实现的
 */
public class DynamicCorsConfiguration implements CorsConfigurationSource {


    private PathMatcher pathMatcher = new AntPathMatcher();

    private UrlPathHelper urlPathHelper = new UrlPathHelper();

    private final Map<String, CorsAntConfig> corsConfigurations = new LinkedHashMap<>();

    private CorsAntConfigProvider crossAntConfigProvider;

    public CorsAntConfigProvider getCrossAntConfigProvider() {
        return crossAntConfigProvider;
    }

    public void setCrossAntConfigProvider(CorsAntConfigProvider crossAntConfigProvider) {
        this.crossAntConfigProvider = crossAntConfigProvider;
    }

    /**
     * Set the PathMatcher implementation to use for matching URL paths
     * against registered URL patterns. Default is AntPathMatcher.
     *
     * @see org.springframework.util.AntPathMatcher
     */
    public void setPathMatcher(PathMatcher pathMatcher) {
        Assert.notNull(pathMatcher, "PathMatcher must not be null");
        this.pathMatcher = pathMatcher;
    }

    /**
     * Shortcut to same property on underlying {@link #setUrlPathHelper UrlPathHelper}.
     *
     * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath
     */
    public void setAlwaysUseFullPath(boolean alwaysUseFullPath) {
        this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath);
    }

    /**
     * Shortcut to same property on underlying {@link #setUrlPathHelper UrlPathHelper}.
     *
     * @see org.springframework.web.util.UrlPathHelper#setUrlDecode
     */
    public void setUrlDecode(boolean urlDecode) {
        this.urlPathHelper.setUrlDecode(urlDecode);
    }

    /**
     * Shortcut to same property on underlying {@link #setUrlPathHelper UrlPathHelper}.
     *
     * @see org.springframework.web.util.UrlPathHelper#setRemoveSemicolonContent(boolean)
     */
    public void setRemoveSemicolonContent(boolean removeSemicolonContent) {
        this.urlPathHelper.setRemoveSemicolonContent(removeSemicolonContent);
    }

    /**
     * Set the UrlPathHelper to use for resolution of lookup paths.
     * <p>Use this to override the default UrlPathHelper with a custom subclass.
     */
    public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
        Assert.notNull(urlPathHelper, "UrlPathHelper must not be null");
        this.urlPathHelper = urlPathHelper;
    }

    /**
     * Set CORS configuration based on URL patterns.
     */
    public void setCorsConfigurations(@Nullable Map<String, CorsAntConfig> corsConfigurations) {
        this.corsConfigurations.clear();
        if (corsConfigurations != null) {
            this.corsConfigurations.putAll(corsConfigurations);
        }
    }

    /**
     * Get the CORS configuration.
     */
    public Map<String, CorsAntConfig> getCorsConfigurations() {
        return Collections.unmodifiableMap(this.corsConfigurations);
    }

    /**
     * Register a {@link CorsConfiguration} for the specified path pattern.
     */
    public void registerCorsConfiguration(String path, CorsAntConfig config) {
        this.corsConfigurations.put(path, config);
    }

    /**
     * 在跨域的配置上，我们做了调整：
     *
     * 1、默认设计，是根据请求的 url，与 ant-path 进行匹配，然后返回对应的跨域配置；
     * 2、当前设计，是根据请求的 origin，找对应的跨域配置；
     *
     * 当前设计，不支持 origin 中有通配符。
     *
     * @param request 请求
     * @return 跨域配置
     */
    @Override
    public CorsConfiguration getCorsConfiguration(@NotNull HttpServletRequest request) {
        String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
        String origin = request.getHeader(HttpHeaders.ORIGIN);

        //  检查请求来源是否满足要求
        //  这是 UrlBasedCorsConfigurationSource 的原始设计，代码我们有做调整
        for (Map.Entry<String, CorsAntConfig> entry : this.corsConfigurations.entrySet()) {
            if (entry.getKey().equals(origin)) {
                CorsAntConfig ret = entry.getValue();
                if (this.pathMatcher.match(ret.getAntPath(), lookupPath)) {
                    return ret.getConfiguration();
                }
            }
        }

        // 尝试获取最新的配置信息
        // 这是我们重新设计之后的逻辑，根据 origin 查找可能的对应的跨域配置
        CorsAntConfig config = crossAntConfigProvider.findCorsAntConfig(origin);
        if (config != null) {
            this.corsConfigurations.put(origin, config);
            return config.getConfiguration();
        }
        return null;
    }
}
